<template>
  <div :class="{ merge }" class="vue-codemirror">
    <div v-if="merge" ref="mergeview"></div>
    <textarea v-else ref="textarea" :name="name" :placeholder="placeholder"></textarea>
  </div>
</template>

<script>
// lib
import _CodeMirror from 'codemirror'

const CodeMirror = window.CodeMirror || _CodeMirror
// pollfill
if (typeof Object.assign != 'function') {
  Object.defineProperty(Object, 'assign', {
    value(target, varArgs) {
      if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object')
      }
      const to = Object(target)
      for (let index = 1; index < arguments.length; index++) {
        const nextSource = arguments[index]
        if (nextSource != null) {
          for (const nextKey in nextSource) {
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey]
            }
          }
        }
      }
      return to
    },
    writable: true,
    configurable: true
  })
}
// export
export default {
  name: 'codemirror-editor',
  data() {
    return {
      content: '',
      codemirror: null,
      instance: null
    }
  },
  props: {
    code: String,
    value: String,
    marker: Function,
    unseenLines: Array,
    name: {type: String, default: 'codemirror'},
    placeholder: {type: String, default: ''},
    merge: {type: Boolean, default: false},
    options: {type: Object, default: () => ({})},
    events: {type: Array, default: () => ([])},
    globalOptions: {type: Object, default: () => ({})},
    globalEvents: {type: Array, default: () => ([])}
  },
  watch: {
    options: {
      deep: true,
      handler(options) {
        for (const key in options) {
          this.instance.setOption(key, options[key])
        }
      }
    },
    merge() {
      this.$nextTick(this.switchMerge)
    },
    code(newVal) {
      this.handerCodeChange(newVal)
    },
    value(newVal) {
      this.handerCodeChange(newVal)
    },
  },
  methods: {
    initialize() {
      const cmOptions = Object.assign({}, this.globalOptions, this.options)
      if (this.merge) {
        this.codemirror = CodeMirror.MergeView(this.$refs.mergeview, cmOptions)
        this.instance = this.codemirror.edit
      } else {
        this.codemirror = CodeMirror.fromTextArea(this.$refs.textarea, cmOptions)
        this.instance = this.codemirror
        this.instance.setValue(this.code || this.value || this.content)
      }
      this.instance.on('change', cm => {
        this.content = cm.getValue()
        if (this.$emit) {
          this.$emit('input', this.content)
        }
      })
      // 所有有效事件（驼峰命名）+ 去重
      const tmpEvents = {}
      const allEvents = [
        'scroll',
        'changes',
        'beforeChange',
        'cursorActivity',
        'keyHandled',
        'inputRead',
        'electricInput',
        'beforeSelectionChange',
        'viewportChange',
        'swapDoc',
        'gutterClick',
        'gutterContextMenu',
        'focus',
        'blur',
        'refresh',
        'optionChange',
        'scrollCursorIntoView',
        'update'
      ]
          .concat(this.events)
          .concat(this.globalEvents)
          .filter(e => (!tmpEvents[e] && (tmpEvents[e] = true)))
          .forEach(event => {
            // 循环事件，并兼容 run-time 事件命名
            this.instance.on(event, (...args) => {
              // console.log('当有事件触发了', event, args)
              this.$emit(event, ...args)
              const lowerCaseEvent = event.replace(/([A-Z])/g, '-$1').toLowerCase()
              if (lowerCaseEvent !== event) {
                this.$emit(lowerCaseEvent, ...args)
              }
            })
          })
      this.$emit('ready', this.codemirror)
      this.unseenLineMarkers()
      // prevents funky dynamic rendering
      this.refresh()
    },
    refresh() {
      this.$nextTick(() => {
        this.instance.refresh()
      })
    },
    destroy() {
      // garbage cleanup
      const element = this.instance.doc.cm.getWrapperElement()
      element && element.remove && element.remove()
    },
    handerCodeChange(newVal) {
      const cm_value = this.instance.getValue()
      if (newVal !== cm_value) {
        const scrollInfo = this.instance.getScrollInfo()
        this.instance.setValue(newVal)
        this.content = newVal
        this.instance.scrollTo(scrollInfo.left, scrollInfo.top)
      }
      this.unseenLineMarkers()
    },
    unseenLineMarkers() {
      if (this.unseenLines !== undefined && this.marker !== undefined) {
        this.unseenLines.forEach(line => {
          const info = this.instance.lineInfo(line)
          this.instance.setGutterMarker(line, 'breakpoints', info.gutterMarkers ? null : this.marker())
        })
      }
    },
    switchMerge() {
      // Save current values
      const history = this.instance.doc.history
      const cleanGeneration = this.instance.doc.cleanGeneration
      this.options.value = this.instance.getValue()
      this.destroy()
      this.initialize()
      // Restore values
      this.instance.doc.history = history
      this.instance.doc.cleanGeneration = cleanGeneration
    }
  },
  mounted() {
    this.initialize()
  },
  beforeDestroy() {
    this.destroy()
  }
}
</script>
